package com.mco.service;

import com.mco.entity.User;
import com.mco.mapepr.UserMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @author ：liao.wei
 * @description：TODO
 * @date ：2023/9/14 21:13
 */
@Service
public class UserService {
	
	public static final String CACHE_KEY_USER = "user:";
	
	@Resource
	private UserMapper userMapper;
	
	@Resource
	private RedisTemplate<String, User> redisTemplate;
	
	/**
	 * 对于中小厂QPS<=1000
	 *
	 * @param id 用户id
	 * @return User
	 */
	public User findById(Integer id) {
		String key = CACHE_KEY_USER + id;
		//1. 从Redis 中查询，有则返回，无则查询数据库
		User user = (User) redisTemplate.opsForValue().get(key);
		
		if (Objects.isNull(user)) {
			//2. Redis 无，差mysql
			user = userMapper.selectById(id);
			if (Objects.isNull(user)) {
				//3.1 Redis+mysql 都无数据
				return user;
			} else {
				//3.2 mysql 有数据，回写Redis，保证下一次命中
				redisTemplate.opsForValue().set(key, user);
			}
		}
		return user;
	}
	
	public User synchronizedFindById(Integer id) {
		String key = CACHE_KEY_USER + id;
		//1. 从Redis 中查询，有则返回，无则查询数据库
		User user = (User) redisTemplate.opsForValue().get(key);
		if (Objects.isNull(user)) {
			//2. 对于高QPS的优化，缓存无数据先加锁，保证只有一个请求，让后续请求等待，防止击穿mysql
			synchronized (UserService.class) {
				// 3 在查询一次数据库，防止加锁时有请求已回写Redis
				user = (User) redisTemplate.opsForValue().get(key);
				if (Objects.isNull(user)) {
					// 4 查询mysql数据库
					user = userMapper.selectById(id);
					if (Objects.isNull(user)) {
						return user;
					} else {
						//5 mysql 有数据，回写Redis，完成数据一致性的同步工作，保证下一次命中,设置过期时间
						redisTemplate.opsForValue().setIfAbsent(key, user, 1L, TimeUnit.DAYS);
					}
				}
			}
		}
		return user;
	}
	
	/**
	 * 测试代码加上sleep，是为了让线程B能够先从数据库读取数据，把缺失的数据写入缓存，
	 * 线程A再进行删除。线程A sleep的时间，需大于线程B读取数据再写入缓存的时间。
	 * 当其它线程读取数据时，会发现缓存缺失，将从数据库中读取最新值。
	 * 此方案会在第一次删除缓存值后，延迟一段时间再进行删除，把它叫做“延迟双删”
	 *
	 * @param user
	 */
	public void updateUser(User user) {
		String key = CACHE_KEY_USER + user.getId();
		try {
			//第一次删除缓存
			redisTemplate.delete(key);
			// 更新数据库
			userMapper.updateById(user);
			try {
				// 模仿网络问题，造成的更新无法立即完成
				TimeUnit.SECONDS.sleep(5);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
			//第二次删除缓存
			redisTemplate.delete(key);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	// 假定数据库一定有数据
	public User selectUser(Integer id) {
		String key = CACHE_KEY_USER + id;
		// 线程B 读取缓存
		User user = redisTemplate.opsForValue().get(key);
		//缓存有数据直接返回
		if (!Objects.isNull(user)) {
			return user;
		}
		// 缓存无数据，读取数据库
		user = userMapper.selectById(id);
		// 线程B 读取 mysql 旧数据，回写Redis，
		redisTemplate.opsForValue().set(key, user);
		return user;
	}
	
	public void updateUserDelay(User user) {
		String key = CACHE_KEY_USER + user.getId();
		//第一次删除缓存
		redisTemplate.delete(key);
		// 更新数据库
		userMapper.updateById(user);
		try {
			CompletableFuture.supplyAsync(() -> {
				//第二次删除缓存,异步删除，，加大吞吐量
				return redisTemplate.delete(key);
			}).whenComplete((t, u) -> {
				System.out.println("t-" + t);
				System.out.println("u-" + u);
			}).exceptionally(e -> false).get();
		} catch (InterruptedException | ExecutionException e) {
			throw new RuntimeException(e);
		}
	}
}
